﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Numerics;
using ArgusLib.Threading;
using ArgusLib.Collections;
using Thread = System.Threading.Thread;

namespace ArgusLib.Fractals.OrbitFractals
{
	public abstract class Fractal : IFractal
	{
		const int listSize = 100;
		const int numberOfThreads = 8;

		public DataSetComplex DataSet { get; private set; }
		private Parameter parameter;
		public Parameter Parameter
		{
			get { return this.parameter; }
			set
			{
				if (value == null)
					throw new ArgumentNullException();
				this.parameter = value;
			}
		}
		private DataSetComplexProvider dataSetProvider;
		public DataSetComplexProvider DataSetProvider
		{
			get { return this.dataSetProvider; }
			set
			{
				if (value == null)
					throw new ArgumentNullException();
				this.dataSetProvider = value;
			}
		}

		IDataSet IFractal.DataSet { get { return this.DataSet; } }
		ArgusLib.Fractals.IParameter IFractal.Parameter { get { return this.Parameter; } set { this.Parameter = (Parameter)value; } }
		IDataSetProvider IFractal.DataSetProvider { get { return this.DataSetProvider; } set { this.DataSetProvider = (DataSetComplexProvider)value; } }

		private ThreadState threadState;
		private Thread thread;

		public bool IsCalculating { get { return this.thread != null; } }

		public Fractal(Parameter parameter)
		{
			if (parameter == null)
				throw new ArgumentNullException();
			this.Parameter = parameter;
			this.DataSetProvider = new DataSetComplexProvider();
		}

		public void StartCalculation()
		{
			if (this.IsCalculating == true)
				throw new Exception("There already is a calculation running.");
			this.thread = new Thread(new System.Threading.ThreadStart(this.Calculate));
			this.threadState = new ThreadState();
			this.thread.Start();
		}

		public void AbortCalculation(bool join)
		{
			this.threadState.RequestCancel();
			if (join == true)
				this.thread.Join();
		}

		public void Calculate()
		{
			DateTime start = DateTime.Now;

			DataSetComplex dataSetBackUp = this.DataSet;
			this.DataSet = this.DataSetProvider.GetDataSet(this.Parameter);

			IEnumerable<Parallelization.TaskBody> tasks = this.GetStartTasks();

			Parallelization.ProcessTasksProgressHandler progressHandler = new Parallelization.ProcessTasksProgressHandler((int tasksProcessed, int tasksToProcess) =>
				{
					double progress = (double)tasksProcessed / (tasksProcessed + tasksToProcess);
					this.OnCalculationProgressChanged(new CalculationProgressChangedEventArgs(progress, DateTime.Now - start, tasksProcessed, tasksToProcess));
				});

			Parallelization.ProcessTasks(tasks, numberOfThreads, progressHandler);

			this.DataSet.PostProcess();

			if (this.threadState.CancelRequested == true)
			{
				this.DataSet = dataSetBackUp;
				this.OnCalculationAborted(new CalculationAbortedEventArgs());
			}
			else
			{
				this.OnCalculationFinished(new CalculationFinishedEventArgs(DateTime.Now - start));
			}
		}

		protected virtual IEnumerable<Parallelization.TaskBody> GetStartTasks()
		{
			List<Parallelization.TaskBody> tasks = new List<Parallelization.TaskBody>();
			ListPair<int, int> coords = new ListPair<int, int>(2*listSize);
			for (int x = 0; x < this.Parameter.PixelWidth; x++)
			{
				coords.Add(x, 0);
				coords.Add(x, this.Parameter.PixelHeight - 1);
				if (coords.Count > listSize)
				{
					tasks.Add(this.GetTask(coords));
					coords = new ListPair<int, int>(2 * listSize);
				}
			}

			for (int y = 0; y < this.Parameter.PixelHeight; y++)
			{
				coords.Add(0, y);
				coords.Add(this.Parameter.PixelWidth - 1, y);
				if (coords.Count > listSize)
				{
					tasks.Add(this.GetTask(coords));
					coords = new ListPair<int, int>(2*listSize);
				}
			}
			if (coords.Count > 0)
				tasks.Add(this.GetTask(coords));

			return tasks;
		}

		private Parallelization.TaskBody GetTask(ListPair<int, int> Coords)
		{
			return new Parallelization.TaskBody((state) =>
				{
					List<Parallelization.TaskBody> newTasks = new List<Parallelization.TaskBody>();
					ListPair<int, int> newCoords = new ListPair<int, int>(2*listSize);
					for (int i = 0; i < Coords.Count; i++)
					{
						if (this.threadState.CancelRequested == true)
						{
							state.RequestCancel();
							return null;
						}

						int x = Coords.List1[i];
						int y = Coords.List2[i];

						if (this.DataSet.IsNull(x, y) == false)
							continue;

						this.IteratePoint(x, y);

						if (this.threadState.CancelRequested == true)
						{
							state.RequestCancel();
							return null;
						}

						if (this.DataSet.Iterations[x, y] < this.Parameter.Iterations) // Not in the set
						{
							if (x > 0)
							{
								if (this.DataSet.IsNull(x - 1, y) == true)
									newCoords.Add(x - 1, y);
							}
							if (y > 0)
							{
								if (this.DataSet.IsNull(x, y - 1) == true)
									newCoords.Add(x, y - 1);
							}
							if (x + 1 < this.DataSet.DimensionX)
							{
								if (this.DataSet.IsNull(x + 1, y) == true)
									newCoords.Add(x + 1, y);
							}
							if (y + 1 < this.DataSet.DimensionY)
							{
								if (this.DataSet.IsNull(x, y + 1) == true)
									newCoords.Add(x, y + 1);
							}

							if (newCoords.Count > listSize)
							{
								Parallelization.TaskBody newTask = this.GetTask(newCoords);
								newTasks.Add(newTask);
								newCoords = new ListPair<int, int>(2*listSize);
							}
						}
					}
					if (newCoords.Count > 0)
					{
						Parallelization.TaskBody newTask = this.GetTask(newCoords);
						newTasks.Add(newTask);
						newCoords = new ListPair<int, int>(2 * listSize);
					}
					return newTasks;
				});
		}

		protected abstract void IteratePoint(int x, int y);

		/// <summary>
		/// Data[0]: int: Number of already processed tasks.
		/// Data[1]: int: Number of tasks that still need to be processed.
		/// </summary>
		public event CalculationProgressChangedEventHandler CalculationProgressChanged;
		public event CalculationAbortedEventHandler CalculationAborted;
		public event CalculationFinishedEventHandler CalculationFinished;

		protected virtual void OnCalculationProgressChanged(CalculationProgressChangedEventArgs e)
		{
			if (this.CalculationProgressChanged != null)
				this.CalculationProgressChanged(this, e);
		}

		protected virtual void OnCalculationAborted(CalculationAbortedEventArgs e)
		{
			this.thread = null;
			if (this.CalculationAborted != null)
				this.CalculationAborted(this, e);
		}

		protected virtual void OnCalculationFinished(CalculationFinishedEventArgs e)
		{
			this.thread = null;
			if (this.CalculationFinished != null)
				this.CalculationFinished(this, e);
		}
	}
}
